/**
 * @file objloader.cpp
 * @brief ObjLoader class methods implementation
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; version 3 of the License.
 *
 * @author VestniK (Sergey N.Vidyuk) sir.vestnik@gmail.com
 * @date 6 Jul 2010
 */
#include "objloader.h"
#include "objloaderprivate.h"

#include <QtCore/QDir>
#include <QtCore/QFile>
#include <QtCore/QFileInfo>
#include <QtCore/QTextStream>
#include <QtCore/QList>
#include <QtCore/QStringList>
#include <QtCore/QRegExp>

#include "mtlloader.h"

using namespace mjn;

/**
 * @brief Constructs empty loader.
 */
ObjLoader::ObjLoader():
    d(new ObjLoaderPrivate)
{
    mValid = false;
    d->error = tr("Not initialized");
}

/**
 * @brief Creates loader and loads *.obj file at the given path.
 */
ObjLoader::ObjLoader(const QString& path):
    d(new ObjLoaderPrivate)
{
    load(path);
}

ObjLoader::~ObjLoader()
{
    delete d;
}

const QString& ObjLoader::errorString() const
{
    return d->error;
}

int ObjLoader::errorLine() const
{
    return d->errorLine;
}

/**
 * @brief Loads file at the given path.
 * 
 * This function tries to load data from the file by the path given. It
 * replaces all data stored in this object but doesn't affects GL list.
 */
bool ObjLoader::load(const QString& path)
{
    mValid = false;
    d->clear();
    QFile file(path);
    if (!file.exists()) {
        d->error = tr("File '%1' doesn't exists").arg(path);
        return false;
    }
    if (!file.open(QIODevice::ReadOnly)) {
        d->error = tr("Can't open '%1' for reading").arg(path);
        return false;
    }
    d->basePath = QFileInfo(file).absolutePath();
    QTextStream stream(&file);

    while (!stream.atEnd()) {
        QString line = stream.readLine();
        d->errorLine++;
        line = line.trimmed();
        if (line.startsWith('#')) continue;
        if (line.isEmpty()) continue;

        if (!d->loadDefinition(line)) return false;
    }

    mValid = true;
    d->errorLine = 0;
    return true;
}

/**
 * Creates new list from the data store in the object returns 0 if this
 * instance is invalid.
 * 
 * If there was another one list created by previous call of this function
 * it will not be deleted. It will not be deleted in destructor as well. You
 * need to call deleteList() function manually.
 * 
 * @note This function should be called when OpenGL initialization is done.
 */
GLuint ObjLoader::buildList()
{
    mList = -1;
    if (!isValid())
        return mList;
    mList = glGenLists(1);
    glNewList(mList,GL_COMPILE);

    foreach (int smoothGrp, d->geometryObjects.keys()) {
        if (smoothGrp != 0) {
            glShadeModel(GL_SMOOTH);
        } else {
            glShadeModel(GL_FLAT);
        }
        QMap<QString,QList<GeometryObject> > objects = d->geometryObjects[smoothGrp];
        QMap<QString,QList<GeometryObject> >::const_iterator it;
        for(it = objects.begin(); it != objects.end(); it++) {
            foreach(GeometryObject object, it.value()) {
                switch (object.type) {
                    case GeometryObject::Points : glBegin(GL_POINTS); break;
                    case GeometryObject::Lines  : glBegin(GL_LINES); break;
                    case GeometryObject::Face   : glBegin(GL_POLYGON); break;
                    default: continue; /// @todo maybe warn user somehow
                }
                /// @todo handle materials here
                glColor3d(0.0, 1.0, 0.0);
                foreach(VertexRef ref, object.vertexRefs) {
                    Vertex v = d->vertexes[ref.vertex-1];
                    glVertex4d(v.x, v.y, v.z, v.w);
                    if (ref.normal != 0) {
                        VertexNormal n = d->normals[ref.normal-1];
                        glNormal3d(n.i, n.j, n.k);
                    }
                    if (ref.textureCoord != 0) {
                        TextureCoord tc = d->textureCoords[ref.textureCoord-1];
                        glTexCoord3d(tc.u, tc.v, tc.w);
                    }
                }
                glEnd();
            }
        }
    }

    glEndList();
    return mList;
}

/**
 * @brief Deletes GL list stored in this instance.
 */
void ObjLoader::deleteList()
{
    if (listAvailable())
        glDeleteLists(mList,1);
}

// Internal functions of ObjLoaderPrivate

void ObjLoaderPrivate::clear() {
    errorLine = 0;
    error = QString();
    currentMaterial = QString();
    currentSmoothingGroup = 0;
    vertexes.clear();
    normals.clear();
    textureCoords.clear();
    geometryObjects.clear();
}

/**
 * @param definition should be trimmed.
 */
bool ObjLoaderPrivate::loadDefinition(const QString &definition)
{
    QStringList tokens = definition.split(QRegExp("\\s+"));
    if (tokens[0] == "v") {
        if (!loadVertex(tokens)) return false;
    } else if (tokens[0] == "vn") {
        if (!loadNormal(tokens)) return false;
    } else if (tokens[0] == "vt") {
        if (!loadTextureCoord(tokens)) return false;
    } else if (tokens[0] == "f" || tokens[0] == "p" || tokens[0] == "l") {
        if (!loadGeomObject(tokens)) return false;
    } else if (tokens[0] == "s") {
        if (!loadSmoothingGroup(tokens)) return false;
    } else if (tokens[0] == "mtllib") {
        if (tokens.size() != 2) {
            error = ObjLoader::tr("Statement 'mtllib' require one paramiter");
            return false;
        }
        QString mtlFilePath = tokens[1];
        if (!QDir::isAbsolutePath(mtlFilePath)) {
            mtlFilePath = QDir(basePath).absoluteFilePath(mtlFilePath);
        }
        if (!QFileInfo(mtlFilePath).isFile()) {
            error = ObjLoader::tr("File '%1' doesn't exists or not a file. Statement 'mtllib' "
                                  "parameter should point to existing file!").arg(mtlFilePath);
            return false;
        }
        MtlLoader loader;
        if (!loader.load(QDir(basePath).absoluteFilePath(tokens[1]))) {
            error = ObjLoader::tr("Failed to load material definitions."
                                  "\nIn file %1:%2 : %3");
            error = error.arg(mtlFilePath).arg(loader.errorLine()).arg(loader.errorString());
            return false;
        }
    } else if (tokens[0] == "usemtl") {
        if (!loadUseMtl(tokens)) return false;
    } else {
        error = ObjLoader::tr("Unknow or unsupported definition: '%1'")
                .arg(definition);
        return false;
    }
    return true;
}

bool ObjLoaderPrivate::loadVertex(const QStringList& tokens)
{
    if (tokens.size()<4 || tokens.size()>5) {
        error = ObjLoader::tr("Wrong vertex format! Should be 'v x y z' or 'v x y z w'\n");
        return false;
    }
    bool ok;
    Vertex v;
    v.x = tokens[1].toDouble(&ok);
    if (!ok) {
        error = ObjLoader::tr("x parameter should be double but get '%1'")
                .arg(tokens[1]);
        return false;
    }
    v.y = tokens[2].toDouble(&ok);
    if (!ok) {
        error = ObjLoader::tr("y parameter should be double but get '%1'")
                .arg(tokens[2]);
        return false;
    }
    v.z = tokens[3].toDouble(&ok);
    if (!ok) {
        error = ObjLoader::tr("z parameter should be double but get '%1'")
                .arg(tokens[3]);
        return false;
    }
    v.w = 1.0;
    if (tokens.size()==5) {
        v.w = tokens[4].toDouble(&ok);
        if (!ok) {
            error = ObjLoader::tr("w parameter should be double but get '%1'")
                    .arg(tokens[4]);
            return false;
        }
    }
    vertexes.append(v);
    return true;
}

bool ObjLoaderPrivate::loadNormal(const QStringList &tokens)
{
    if (tokens.size() != 4) {
        error = ObjLoader::tr("Wrong vertex normal format! Should be 'vn i j k'\n");
        return false;
    }
    bool ok;
    VertexNormal n;
    n.i = tokens[1].toDouble(&ok);
    if (!ok) {
        error = ObjLoader::tr("i parameter should be double but get '%1'")
                .arg(tokens[1]);
        return false;
    }
    n.j = tokens[2].toDouble(&ok);
    if (!ok) {
        error = ObjLoader::tr("j parameter should be double but get '%1'")
                .arg(tokens[2]);
        return false;
    }
    n.k = tokens[3].toDouble(&ok);
    if (!ok) {
        error = ObjLoader::tr("k parameter should be double but get '%1'")
                .arg(tokens[3]);
        return false;
    }
    normals.append(n);
    return true;
}

bool ObjLoaderPrivate::loadTextureCoord(const QStringList& tokens)
{
    if (tokens.size()<2 || tokens.size()>4) {
        error = ObjLoader::tr("Wrong texture vertex format! Should be 'vt u', 'vt u v' or 'vt u v w'\n");
        return false;
    }
    bool ok;
    TextureCoord vt;
    vt.u = tokens[1].toDouble(&ok);
    if (!ok) {
        error = ObjLoader::tr("u parameter should be double but get '%1'")
                .arg(tokens[1]);
        return false;
    }
    vt.v = 0.0;
    if (tokens.size()>2) {
        vt.v = tokens[2].toDouble(&ok);
        if (!ok) {
            error = ObjLoader::tr("w parameter should be double but get '%1'")
                    .arg(tokens[2]);
            return false;
        }
    }
    vt.w = 0.0;
    if (tokens.size()>3) {
        vt.w = tokens[3].toDouble(&ok);
        if (!ok) {
            error = ObjLoader::tr("w parameter should be double but get '%1'")
                    .arg(tokens[3]);
            return false;
        }
    }
    textureCoords.append(vt);
    return true;
}

bool ObjLoaderPrivate::loadSmoothingGroup(const QStringList &tokens) {
    if (tokens.size() != 2) {
        error = ObjLoader::tr("Wrong smoothing group format! Should be 's group_number'\n");
        return false;
    }
    if (tokens[1] == "off") {
        currentSmoothingGroup = 0;
        return true;
    }
    bool ok;
    currentSmoothingGroup = tokens[1].toInt(&ok);
    if (!ok) {
        error = ObjLoader::tr("smoothing group number should be 'off' or positive integer but get: '%1'")
                .arg(tokens[1]);
        return false;
    }
    if (currentSmoothingGroup < 0) {
        error = ObjLoader::tr("smoothing group number should be 'off' or positive integer but get: '%1'")
                .arg(tokens[1]);
        return false;
    }
    return true;
}

bool ObjLoaderPrivate::loadUseMtl(const QStringList &tokens) {
    if (tokens.size() != 2) {
        error = ObjLoader::tr("Wrong usemtl format! Should be 'usemtl mat_name'\n");
        return false;
    }
    /// @todo validate that material exists!
    currentMaterial = tokens[1];
    return true;
}

bool ObjLoaderPrivate::loadGeomObject(const QStringList &tokens) {
    if (tokens.size() < 2) {
        error = ObjLoader::tr("No arguments for the face declaration!");
        return false;
    }
    GeometryObject g;
    if (tokens[0] == "f") {
        g.type = GeometryObject::Face;
    } else if (tokens[0] == "p") {
        g.type = GeometryObject::Points;
    } else if (tokens[0] == "l") {
        g.type = GeometryObject::Lines;
    } else {
        error = ObjLoader::tr("Unknown geometry object type: '%1'").arg(tokens[0]);
        return false;
    }
    bool ok;
    for (int i=1; i<tokens.size(); i++) {
        QStringList vertexRefs = tokens[i].split('/');
        VertexRef vr;
        vr.vertex = vertexRefs[0].toInt(&ok);
        if (!ok || vr.vertex < 1 || vr.vertex > vertexes.size()+1) {
            error = ObjLoader::tr("Invalid reference to vertex '%1' should be integer between 1 and %2")
                    .arg(vertexRefs[0])
                    .arg(vertexes.size()+1);
            return false;
        }
        if (vertexRefs[1].isEmpty()) {
            vr.textureCoord = 0;
        } else {
            vr.textureCoord = vertexRefs[1].toInt(&ok);
            if (!ok || vr.vertex < 1 || vr.vertex > vertexes.size()+1) {
                error = ObjLoader::tr("Invalid reference to texture vertex '%1' should be integer between 1 and %2")
                        .arg(vertexRefs[1])
                        .arg(vertexes.size()+1);
                return false;
            }
        }
        if (vertexRefs[2].isEmpty()) {
            vr.normal = 0;
        } else {
            vr.normal = vertexRefs[2].toInt(&ok);
            if (!ok || vr.vertex < 1 || vr.vertex > vertexes.size()+1) {
                error = ObjLoader::tr("Invalid reference to texture vertex '%1' should be integer between 1 and %2")
                        .arg(vertexRefs[2])
                        .arg(vertexes.size()+1);
                return false;
            }
        }
        g.vertexRefs.append(vr);
    }
    geometryObjects[currentSmoothingGroup][currentMaterial].append(g);
    return true;
}
